using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using static LightCAD.MathLib.Constants;

namespace LightCAD.Three
{
    public class AnimationAction
    {

        #region Properties

        public AnimationMixer _mixer;
        public AnimationClip _clip;
        public IAnimationObject _localRoot;
        public int blendMode;
        public InterpolantSettings _interpolantSettings;
        public ListEx<Interpolant> _interpolants;
        public ListEx<PropertyMixer> _propertyBindings;
        public int? _cacheIndex;
        public int? _byClipCacheIndex;
        public Interpolant _timeScaleInterpolant;
        public Interpolant _weightInterpolant;
        public int loop;
        public int _loopCount;
        public double? _startTime;
        public double time;
        public double timeScale;
        public double _effectiveTimeScale;
        public double weight;
        public double _effectiveWeight;
        public int repetitions;
        public bool paused;
        public bool enabled;
        public bool clampWhenFinished;
        public bool zeroSlopeAtStart;
        public bool zeroSlopeAtEnd;

        #endregion

        #region constructor
        public AnimationAction(AnimationMixer mixer, AnimationClip clip, IAnimationObject localRoot = null, int? blendMode = null)
        {
            if (blendMode == null) blendMode = clip.blendMode;
            this._mixer = mixer;
            this._clip = clip;
            this._localRoot = localRoot;
            this.blendMode = blendMode.Value;
            var tracks = clip.tracks;
            var nTracks = tracks.Length;
            var interpolants = new ListEx<Interpolant>(nTracks);
            var interpolantSettings = new InterpolantSettings
            {
                EndingStart = ZeroCurvatureEnding,
                EndingEnd = ZeroCurvatureEnding
            };
            for (int i = 0; i != nTracks; ++i)
            {
                var interpolant = tracks[i].createInterpolant(null);
                interpolants[i] = interpolant;
                interpolant.Settings = interpolantSettings;
            }
            this._interpolantSettings = interpolantSettings;
            this._interpolants = interpolants; // bound by the mixer
                                               // inside: PropertyMixer (managed by the mixer)
            this._propertyBindings = new ListEx<PropertyMixer>(nTracks);
            this._cacheIndex = null; // for the memory manager
            this._byClipCacheIndex = null; // for the memory manager
            this._timeScaleInterpolant = null;
            this._weightInterpolant = null;
            this.loop = LoopRepeat;
            this._loopCount = -1;
            // global mixer time when the action is to be started
            // it"s set back to "null" upon start of the action
            this._startTime = null;
            // scaled local time of the action
            // gets clamped or wrapped to 0..clip.duration according to loop
            this.time = 0;
            this.timeScale = 1;
            this._effectiveTimeScale = 1;
            this.weight = 1;
            this._effectiveWeight = 1;
            this.repetitions = MathEx.Infinity; // no. of repetitions when looping
            this.paused = false; // true -> zero effective time scale
            this.enabled = true; // false -> zero effective weight
            this.clampWhenFinished = false;// keep feeding the last frame?
            this.zeroSlopeAtStart = true;// for smooth interpolation w/o separate
            this.zeroSlopeAtEnd = true;// clips for start, loop and end
        }
        #endregion

        #region methods
        public AnimationAction play()
        {
            this._mixer._activateAction(this);
            return this;
        }
        public object stop()
        {
            this._mixer._deactivateAction(this);
            return this.reset();
        }
        public object reset()
        {
            this.paused = false;
            this.enabled = true;
            this.time = 0; // restart clip
            this._loopCount = -1;// forget previous loops
            this._startTime = null;// forget scheduling
            return this.stopFading().stopWarping();
        }
        public bool isRunning()
        {
            return this.enabled && !this.paused && this.timeScale != 0 &&
                this._startTime == null && this._mixer._isActiveAction(this);
        }
        public bool isScheduled()
        {
            return this._mixer._isActiveAction(this);
        }
        public AnimationAction startAt(double time)
        {
            this._startTime = time;
            return this;
        }
        public AnimationAction setLoop(int mode, int repetitions)
        {
            this.loop = mode;
            this.repetitions = repetitions;
            return this;
        }
        public object setEffectiveWeight(double weight)
        {
            this.weight = weight;
            // note: same logic as when updated at runtime
            this._effectiveWeight = this.enabled ? weight : 0.0;
            return this.stopFading();
        }
        public double getEffectiveWeight()
        {
            return this._effectiveWeight;
        }
        public object fadeIn(double duration)
        {
            return this._scheduleFading(duration, 0, 1);
        }
        public object fadeOut(double duration)
        {
            return this._scheduleFading(duration, 1, 0);
        }
        public AnimationAction crossFadeFrom(AnimationAction fadeOutAction, double duration, bool warp)
        {
            fadeOutAction.fadeOut(duration);
            this.fadeIn(duration);
            if (warp)
            {
                var fadeInDuration = this._clip.duration;
                var fadeOutDuration = fadeOutAction._clip.duration;
                var startEndRatio = fadeOutDuration / fadeInDuration;
                var endStartRatio = fadeInDuration / fadeOutDuration;
                fadeOutAction.warp(1.0, startEndRatio, duration);
                this.warp(endStartRatio, 1.0, duration);
            }
            return this;
        }
        public AnimationAction crossFadeTo(AnimationAction fadeInAction, double duration, bool warp)
        {
            return fadeInAction.crossFadeFrom(this, duration, warp);
        }
        public AnimationAction stopFading()
        {
            var weightInterpolant = this._weightInterpolant;
            if (weightInterpolant != null)
            {
                this._weightInterpolant = null;
                this._mixer._takeBackControlInterpolant(weightInterpolant);
            }
            return this;
        }
        public object setEffectiveTimeScale(double timeScale)
        {
            this.timeScale = timeScale;
            this._effectiveTimeScale = this.paused ? 0 : timeScale;
            return this.stopWarping();
        }
        public object getEffectiveTimeScale()
        {
            return this._effectiveTimeScale;
        }
        public AnimationAction setDuration(double duration)
        {
            this.timeScale = this._clip.duration / duration;
            return this.stopWarping();
        }
        public AnimationAction syncWith(AnimationAction action)
        {
            this.time = action.time;
            this.timeScale = action.timeScale;
            return this.stopWarping();
        }
        public object halt(double duration)
        {
            return this.warp(this._effectiveTimeScale, 0, duration);
        }
        public AnimationAction warp(double startTimeScale, double endTimeScale, double duration)
        {
            var mixer = this._mixer;
            var now = mixer.time;
            var timeScale = this.timeScale;
            var interpolant = this._timeScaleInterpolant;
            if (interpolant == null)
            {
                interpolant = mixer._lendControlInterpolant();
                this._timeScaleInterpolant = interpolant;
            }
            var times = interpolant.ParameterPositions;
            var values = interpolant.SampleValues;
            times[0] = now;
            times[1] = now + duration;
            values[0] = startTimeScale / timeScale;
            values[1] = endTimeScale / timeScale;
            return this;
        }
        public AnimationAction stopWarping()
        {
            var timeScaleInterpolant = this._timeScaleInterpolant;
            if (timeScaleInterpolant != null)
            {
                this._timeScaleInterpolant = null;
                this._mixer._takeBackControlInterpolant(timeScaleInterpolant);
            }
            return this;
        }
        public object getMixer()
        {
            return this._mixer;
        }
        public AnimationClip getClip()
        {
            return this._clip;
        }
        public IAnimationObject getRoot()
        {
            return this._localRoot ?? this._mixer._root;
        }
        public void _update(double time, double deltaTime, double timeDirection, int accuIndex)
        {
            // called by the mixer
            if (!this.enabled)
            {
                // call ._updateWeight() to update ._effectiveWeight
                this._updateWeight(time);
                return;
            }
            var startTime = this._startTime;
            if (startTime != null)
            {
                // check for scheduled start of action
                var timeRunning = (time - startTime.Value) * timeDirection;
                if (timeRunning < 0 || timeDirection == 0)
                {
                    deltaTime = 0;
                }
                else
                {
                    this._startTime = null; // unschedule
                    deltaTime = timeDirection * timeRunning;
                }
            }
            // apply time scale and advance time
            deltaTime *= this._updateTimeScale(time);
            var clipTime = this._updateTime(deltaTime);
            // note: _updateTime may disable the action resulting in
            // an effective weight of 0
            var weight = this._updateWeight(time);
            if (weight > 0)
            {
                var interpolants = this._interpolants;
                var propertyMixers = this._propertyBindings;
                switch (this.blendMode)
                {
                    case AdditiveAnimationBlendMode:
                        for (int j = 0, m = interpolants.Length; j != m; ++j)
                        {
                            interpolants[j].Evaluate(clipTime);
                            propertyMixers[j].accumulateAdditive(weight);
                        }
                        break;
                    case NormalAnimationBlendMode:
                    default:
                        for (int j = 0, m = interpolants.Length; j != m; ++j)
                        {
                            interpolants[j].Evaluate(clipTime);
                            propertyMixers[j].accumulate(accuIndex, weight);
                        }
                        break;
                }
            }
        }
        public double _updateWeight(double time)
        {
            double weight = 0;
            if (this.enabled)
            {
                weight = this.weight;
                var interpolant = this._weightInterpolant;
                if (interpolant != null)
                {
                    var interpolantValue = interpolant.Evaluate(time)[0];
                    weight *= interpolantValue;
                    if (time > interpolant.ParameterPositions[1])
                    {
                        this.stopFading();
                        if (interpolantValue == 0)
                        {
                            // faded out, disable
                            this.enabled = false;
                        }
                    }
                }
            }
            this._effectiveWeight = weight;
            return weight;
        }
        public double _updateTimeScale(double time)
        {
            double timeScale = 0;
            if (!this.paused)
            {
                timeScale = this.timeScale;
                var interpolant = this._timeScaleInterpolant;
                if (interpolant != null)
                {
                    var interpolantValue = interpolant.Evaluate(time)[0];
                    timeScale *= interpolantValue;
                    if (time > interpolant.ParameterPositions[1])
                    {
                        this.stopWarping();
                        if (timeScale == 0)
                        {
                            // motion has halted, pause
                            this.paused = true;
                        }
                        else
                        {
                            // warp done - apply final time scale
                            this.timeScale = timeScale;
                        }
                    }
                }
            }
            this._effectiveTimeScale = timeScale;
            return timeScale;
        }
        public double _updateTime(double deltaTime)
        {
            var duration = this._clip.duration;
            var loop = this.loop;
            var time = this.time + deltaTime;
            var loopCount = this._loopCount;
            var pingPong = (loop == LoopPingPong);
            if (deltaTime == 0)
            {
                if (loopCount == -1) return time;
                return (pingPong && (loopCount & 1) == 1) ? duration - time : time;
            }
            if (loop == LoopOnce)
            {
                if (loopCount == -1)
                {
                    // just started
                    this._loopCount = 0;
                    this._setEndings(true, true, false);
                }
            handle_stop:
                {
                    if (time >= duration)
                    {
                        time = duration;
                    }
                    else if (time < 0)
                    {
                        time = 0;
                    }
                    else
                    {
                        this.time = time;
                        goto handle_stop;
                    }
                    if (this.clampWhenFinished) this.paused = true;
                    else this.enabled = false;
                    this.time = time;
                    this._mixer.dispatchEvent(new EventArgs
                    {
                        type = "finished",
                        args = new JsObj<object>{
                                { "action",this },
                                {"direction", deltaTime < 0 ? -1 : 1 }
                            }
                    });
                }
            }
            else
            { // repetitive Repeat or PingPong
                if (loopCount == -1)
                {
                    // just started
                    if (deltaTime >= 0)
                    {
                        loopCount = 0;
                        this._setEndings(true, this.repetitions == 0, pingPong);
                    }
                    else
                    {
                        // when looping in reverse direction, the initial
                        // transition through zero counts as a repetition,
                        // so leave loopCount at -1
                        this._setEndings(this.repetitions == 0, true, pingPong);
                    }
                }
                if (time >= duration || time < 0)
                {
                    // wrap around
                    var loopDelta = Math.Floor(time / duration); // signed
                    time -= duration * loopDelta;
                    loopCount += (int)Math.Abs(loopDelta);
                    var pending = this.repetitions - loopCount;
                    if (pending <= 0)
                    {
                        // have to stop (switch state, clamp time, fire event)
                        if (this.clampWhenFinished) this.paused = true;
                        else this.enabled = false;
                        time = deltaTime > 0 ? duration : 0;
                        this.time = time;
                        this._mixer.dispatchEvent(new EventArgs
                        {
                            type = "finished",
                            args = new JsObj<object>{
                                    { "action", this },
                                    {"direction", deltaTime > 0 ? 1 : -1 }
                                }
                        });
                    }
                    else
                    {
                        // keep running
                        if (pending == 1)
                        {
                            // entering the last round
                            var atStart = deltaTime < 0;
                            this._setEndings(atStart, !atStart, pingPong);
                        }
                        else
                        {
                            this._setEndings(false, false, pingPong);
                        }
                        this._loopCount = loopCount;
                        this.time = time;
                        this._mixer.dispatchEvent(new EventArgs
                        {
                            type = "loop",
                            args = new JsObj<object>{
                                    { "action", this },
                                    {"loopDelta", loopDelta }
                                }
                        });
                    }
                }
                else
                {
                    this.time = time;
                }
                if (pingPong && (loopCount & 1) == 1)
                {
                    // invert time for the "pong round"
                    return duration - time;
                }
            }
            return time;
        }
        public void _setEndings(bool atStart, bool atEnd, bool pingPong)
        {
            var settings = this._interpolantSettings;
            if (pingPong)
            {
                settings.EndingStart = ZeroSlopeEnding;
                settings.EndingEnd = ZeroSlopeEnding;
            }
            else
            {
                // assuming for LoopOnce atStart == atEnd 
                if (atStart)
                {
                    settings.EndingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;
                }
                else
                {
                    settings.EndingStart = WrapAroundEnding;
                }
                if (atEnd)
                {
                    settings.EndingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;
                }
                else
                {
                    settings.EndingEnd = WrapAroundEnding;
                }
            }
        }
        public AnimationAction _scheduleFading(double duration, double weightNow, double weightThen)
        {
            var mixer = this._mixer;
            var now = mixer.time;
            var interpolant = this._weightInterpolant;
            if (interpolant == null)
            {
                interpolant = mixer._lendControlInterpolant();
                this._weightInterpolant = interpolant;
            }
            var times = interpolant.ParameterPositions;
            var values = interpolant.SampleValues;
            times[0] = now;
            values[0] = weightNow;
            times[1] = now + duration;
            values[1] = weightThen;
            return this;
        }
        #endregion
    }
}
